package org.ovirt.engine.core.utils;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import javax.inject.Inject;
import javax.inject.Singleton;

import org.ovirt.engine.core.common.businessentities.ArchitectureType;
import org.ovirt.engine.core.common.businessentities.OriginType;
import org.ovirt.engine.core.common.businessentities.OvfEntityData;
import org.ovirt.engine.core.common.businessentities.VMStatus;
import org.ovirt.engine.core.common.businessentities.VmBase;
import org.ovirt.engine.core.common.businessentities.VmEntityType;
import org.ovirt.engine.core.common.businessentities.storage.UnregisteredDisk;
import org.ovirt.engine.core.common.osinfo.OsRepository;
import org.ovirt.engine.core.compat.Guid;
import org.ovirt.engine.core.utils.archivers.tar.TarInMemoryExport;
import org.ovirt.engine.core.utils.ovf.OvfInfoFileConstants;
import org.ovirt.engine.core.utils.ovf.xml.XmlDocument;
import org.ovirt.engine.core.utils.ovf.xml.XmlNode;
import org.ovirt.engine.core.utils.ovf.xml.XmlNodeList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

@Singleton
public class OvfUtils {
    private static final String TEMPLATE_ENTITY_TYPE = "<TemplateType>";
    private static final String ENTITY_NAME = "<Name>";
    private static final String VM_ORIGIN = "Origin";
    private static final String END_ENTITY_NAME = "</Name>";
    private static final String OVF_FILE_EXT = ".ovf";
    private static final int GUID_LENGTH = Guid.Empty.toString().length();
    protected static final Logger log = LoggerFactory.getLogger(TarInMemoryExport.class);

    @Inject
    private OsRepository osRepository;

    private static String getEntityName(String ovfData) {
        int beginIndexOfEntityName = ovfData.indexOf(ENTITY_NAME) + ENTITY_NAME.length();
        int endIndexOfEntityName = ovfData.indexOf(END_ENTITY_NAME, beginIndexOfEntityName);
        String entityName = ovfData.substring(beginIndexOfEntityName, endIndexOfEntityName);
        return entityName;
    }

    private static VmEntityType getVmEntityType(String ovfData) {
        VmEntityType vmEntityType = VmEntityType.VM;
        int indexOfEntityType = ovfData.indexOf(TEMPLATE_ENTITY_TYPE);
        if (indexOfEntityType != -1) {
            vmEntityType = VmEntityType.TEMPLATE;
        }
        return vmEntityType;
    }

    public Set<Guid> fetchVmDisks(XmlDocument xmlDocument) {
        Set<Guid> disksIds = new HashSet<>();
        XmlNode references = xmlDocument.selectSingleNode("//*/References");
        // we assume that all files in OVFs that are generated by oVirt are disks
        for (XmlNode file : references.selectNodes("File")) {
            disksIds.add(Guid.createGuidFromString(file.attributes.get("ovf:href").getValue().substring(0, GUID_LENGTH)));
        }
        disksIds.addAll(fetchMemoryDisks(xmlDocument));
        return disksIds;
    }

    private static Guid getEntityId(String fileName) {
        return Guid.createGuidFromString(fileName.substring(0, fileName.length() - OVF_FILE_EXT.length()));
    }

    private static OvfEntityData createOvfEntityData(Guid storageDomainId,
            String ovfData,
            VmEntityType vmEntityType,
            String entityName,
            ArchitectureType archType,
            Guid entityId) {
        OvfEntityData ovfEntityData = new OvfEntityData();
        ovfEntityData.setOvfData(ovfData);
        ovfEntityData.setEntityType(vmEntityType);
        ovfEntityData.setEntityName(entityName);
        ovfEntityData.setStorageDomainId(storageDomainId);
        ovfEntityData.setArchitecture(archType);
        ovfEntityData.setEntityId(entityId);
        return ovfEntityData;
    }

    public List<OvfEntityData> getOvfEntities(byte[] tar,
            List<UnregisteredDisk> unregisteredDisks,
            Guid storageDomainId) {
        List<OvfEntityData> ovfEntityDataFromTar = new ArrayList<>();
        InputStream is = new ByteArrayInputStream(tar);

        log.info("Start fetching files from tar file");
        Map<String, ByteBuffer> filesFromTar;
        try (TarInMemoryExport memoryTar = new TarInMemoryExport(is)) {
            filesFromTar = memoryTar.unTar();
        } catch (IOException e) {
            throw new RuntimeException(String.format("Exception while getting OVFs files from tar file for domain %s",
                    storageDomainId), e);
        }

        Entry<String, ByteBuffer> metaDataFileEntry = null;
        for (Entry<String, ByteBuffer> fileEntry : filesFromTar.entrySet()) {
            if (fileEntry.getKey().endsWith(OVF_FILE_EXT)) {
                analyzeOvfFile(unregisteredDisks, storageDomainId, ovfEntityDataFromTar, fileEntry);
            } else if (fileEntry.getKey().equals(OvfInfoFileConstants.MetaDataFileName)) {
                metaDataFileEntry = fileEntry;
            } else {
                log.info("File '{}' is not an OVF file, will be ignored.", fileEntry.getKey());
            }
        }
        analyzeOvfMetaDataFile(storageDomainId, ovfEntityDataFromTar, metaDataFileEntry);
        log.info("Finish to fetch OVF files from tar file. The number of OVF entities are {}",
                ovfEntityDataFromTar.size());
        return ovfEntityDataFromTar;
    }

    private void analyzeOvfMetaDataFile(Guid storageDomainId, List<OvfEntityData> ovfEntityDataFromTar, Entry<String, ByteBuffer> metaDataFileEntry) {
        if (metaDataFileEntry != null) {
            log.info("Start to analyze metadata file '{}'.", metaDataFileEntry.getKey());
            initStorageOvfExtraData(storageDomainId, ovfEntityDataFromTar, metaDataFileEntry.getValue());
            log.info("Finish to analyze metadata File '{}'.", metaDataFileEntry.getKey());
        } else {
            log.debug("No metadata file found in tar file");
        }
    }

    public Set<Guid> fetchMemoryDisks(XmlDocument xmlDocument) {
        Set<Guid> memoryDiskIds = new HashSet<>();
        xmlDocument.selectNodes("ovf:SnapshotsSection_Type");
        XmlNode content = xmlDocument.selectSingleNode("//*/Content");
        XmlNodeList nodeList = content.selectNodes("Section");
        if (nodeList != null) {
            for (XmlNode section : nodeList) {
                String value = section.attributes.get("xsi:type").getValue();
                if (value.equals("ovf:SnapshotsSection_Type")) {
                    Iterator<XmlNode> snapshotIter = section.selectNodes("Snapshot").iterator();
                    while (snapshotIter.hasNext()) {
                        XmlNode memorySnapshot = snapshotIter.next().selectSingleNode("Memory");
                        if (memorySnapshot != null) {
                            List<Guid> guids = Guid.createGuidListFromString(memorySnapshot.innerText);
                            memoryDiskIds.add(guids.get(2));
                            memoryDiskIds.add(guids.get(4));
                        }
                    }
                }
            }
        }
        return memoryDiskIds;
    }

    private void initStorageOvfExtraData(Guid storageDomainId,
            List<OvfEntityData> ovfEntityDataFromTar,
            ByteBuffer metaDataBuffer) {
        Map<String, Object> diskDescriptionMap;
        String storageMetaData = new String(metaDataBuffer.array());
        try {
            diskDescriptionMap = JsonHelper.jsonToMap(storageMetaData);
        } catch (IOException e) {
            log.error("Failed to convert storage ovf extra data from json to map: '{}'.", storageMetaData);
            e.printStackTrace();
            return;
        }
        setVmsStatus(storageDomainId, diskDescriptionMap, ovfEntityDataFromTar);
    }

    private void setVmsStatus(Guid storageDomainId,
            Map<String, Object> diskDescriptionMap,
            List<OvfEntityData> ovfEntityDataFromTar) {
        Map<Guid, VMStatus> vmsStatusesMap = new HashMap<>();
        Map<String, Object> vmsStatus = (Map<String, Object>) diskDescriptionMap.get(OvfInfoFileConstants.VmStatus);
        if (vmsStatus == null) {
            log.error("VMs status could not be fetched from metadata json file for storage id '{}'.", storageDomainId);
        } else {
            for (Map.Entry<String, Object> entry : vmsStatus.entrySet()) {

                log.debug("VM '{}' fetched from metadata json file with status '{}' for storage domain id '{}.",
                        entry.getKey(),
                        entry.getValue(),
                        storageDomainId);
                vmsStatusesMap.put(Guid.createGuidFromString(entry.getKey()),
                        VMStatus.forValue((Integer) entry.getValue()));
            }
        }
        ovfEntityDataFromTar.forEach(ovfEntity -> ovfEntity.setStatus(vmsStatusesMap.get(ovfEntity.getEntityId())));
    }

    private void analyzeOvfFile(List<UnregisteredDisk> unregisteredDisks,
            Guid storageDomainId,
            List<OvfEntityData> ovfEntityDataFromTar,
            Entry<String, ByteBuffer> fileEntry) {
        String ovfData = new String(fileEntry.getValue().array());
        VmEntityType vmType = getVmEntityType(ovfData);
        ArchitectureType archType = null;
        Guid entityId = getEntityId(fileEntry.getKey());
        String vmName = getEntityName(ovfData);
        try {
            XmlDocument xmlDocument = new XmlDocument(ovfData);
            archType = getOsSection(xmlDocument);
            if (isExternalVM(xmlDocument)) {
                log.warn(
                        "Retrieve an external OVF Entity from storage domain ID '{}' for entity ID '{}'," +
                                " entity name '{}' and VM Type of '{}'." +
                                " This OVF will be ignored since external VMs should not be restored.",
                        storageDomainId,
                        getEntityId(fileEntry.getKey()),
                        getEntityName(ovfData),
                        vmType.name());
                return;
            }
            updateUnregisteredDisksWithVMs(unregisteredDisks, entityId, vmName, xmlDocument);
        } catch (Exception e) {
            log.error("Could not parse VM's disks or architecture, file name: {}, content size: {}, error: {}",
                    fileEntry.getKey(),
                    ovfData.length(),
                    e.getMessage());
            log.debug("Exception", e);
            return;
        }
        // Creates an OVF entity data.
        OvfEntityData ovfEntityData =
                createOvfEntityData(storageDomainId,
                        ovfData,
                        vmType,
                        vmName,
                        archType,
                        entityId);
        log.info(
                "Retrieve OVF Entity from storage domain ID '{}' for entity ID '{}', entity name '{}' and VM Type of '{}'",
                storageDomainId,
                getEntityId(fileEntry.getKey()),
                getEntityName(ovfData),
                vmType.name());
        ovfEntityDataFromTar.add(ovfEntityData);
    }

    public boolean isExternalVM(XmlDocument xmlDocument) {
        XmlNode content = xmlDocument.selectSingleNode("//*/Content");
        NodeList nodeList = content.getChildNodes();
        for (int i = 0; i < nodeList.getLength(); i++) {
            Node node = nodeList.item(i);
            if (node.getNodeName().equals(VM_ORIGIN) && node.getChildNodes().item(0) != null) {
                Integer originType = Integer.valueOf(node.getChildNodes().item(0).getNodeValue());
                return OriginType.EXTERNAL == OriginType.forValue(originType);
            }
        }
        return false;
    }

    public void updateUnregisteredDisksWithVMs(List<UnregisteredDisk> unregisteredDisks,
            Guid entityId,
            String vmName,
            XmlDocument xmlDocument) {
        for (Guid diskId : fetchVmDisks(xmlDocument)) {
            UnregisteredDisk unregisterDisk = unregisteredDisks.stream()
                    .filter(unregrDisk -> diskId.equals(unregrDisk.getDiskId()))
                    .findAny()
                    .orElse(null);
            VmBase vm = new VmBase();
            vm.setId(entityId);
            vm.setName(vmName);
            if (unregisterDisk != null) {
                unregisterDisk.getVms().add(vm);
            }
        }
    }

    private ArchitectureType getOsSection(XmlDocument xmlDocument) {
        ArchitectureType archType = null;
        XmlNode content = xmlDocument.selectSingleNode("//*/Content");
        XmlNodeList nodeList = content.selectNodes("Section");
        XmlNode selectedSection = null;
        if (nodeList != null) {
            for (XmlNode section : nodeList) {
                String value = section.attributes.get("xsi:type").getValue();

                if (value.equals("ovf:OperatingSystemSection_Type")) {
                    selectedSection = section;
                    break;
                }
            }
            if (selectedSection != null) {
                int osId = osRepository.getOsIdByUniqueName(selectedSection.innerText);
                archType = osRepository.getArchitectureFromOS(osId);
            }
        }
        return archType;
    }
}
